/**
 * 
 */
package com.williamoneil.charts.client.util;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import com.google.gwt.canvas.dom.client.Context2d;
import com.williamoneil.charts.client.data.BenchmarkAxis;
import com.williamoneil.charts.client.data.BenchmarkLine;
import com.williamoneil.charts.client.data.ChartBoundsRect;
import com.williamoneil.charts.client.data.ChartElementsData;
import com.williamoneil.charts.client.data.DateAxis;
import com.williamoneil.charts.client.data.MovingAverage;
import com.williamoneil.charts.client.data.Price;
import com.williamoneil.charts.client.data.RelativeStrengthLine;
import com.williamoneil.charts.client.data.Tick;
import com.williamoneil.charts.client.data.Volume;
import com.williamoneil.charts.client.widget.helper.DailyChartHelper;
import com.williamoneil.charts.client.widget.helper.IChartHelper;
import com.williamoneil.charts.client.widget.helper.MonthlyChartHelper;
import com.williamoneil.charts.client.widget.helper.WeeklyChartHelper;
import com.williamoneil.charts.shared.ChartData;
import com.williamoneil.charts.shared.PeriodicityType;
import com.williamoneil.charts.shared.PriceVolumeData;
import com.williamoneil.charts.shared.TradeDateType;

/**
 * @author Gudipati
 *
 */
public class ChartUtil {
	
	public static final short DEFAULT_TICK_SPACING = 5;
	public static final short NUM_TICKS_EXTRA = 10;
	public static final double RS_FACTOR = 0.9;

	private ChartUtil(){
	}
	
	public static ChartElementsData getChartElements(final ChartData chartData, final ChartData benchmarkChartData, final ChartBoundsRect chartBounds, final boolean isLinear, final int tickSpacing) {
		// if we dont have any chart-specific data, lets return this method
		if(chartBounds == null || chartData == null || chartData.getPeriodicity() == null || chartData.getPrices() == null || chartData.getPrices().size() == 0) {
			return null;
		}
		
		final PeriodicityType periodicity = chartData.getPeriodicity();
		final ChartElementsData elements = new ChartElementsData();
		elements.setPeriodicity(periodicity);
		final List<PriceVolumeData> pvList = chartData.getPrices();
		
		PriceMovingAverageHelper priceMA1 = null;
		PriceMovingAverageHelper priceMA2 = null;
		switch(periodicity) {
		case Daily : {
			priceMA1 = new PriceMovingAverageHelper(50, true);
			priceMA2 = new PriceMovingAverageHelper(200, false);
			break;
		}
		case Weekly: {
			priceMA1 = new PriceMovingAverageHelper(10, true);
			priceMA2 = new PriceMovingAverageHelper(40, false);
			break;			
		}
		case Monthly: {
			priceMA1 = null;
			priceMA2 = null;
			break;			
		}	
		}
		final PriceTickLabelHelper priceTickLabelHelper = new PriceTickLabelHelper();
		
		final List<Tick> ticks = new ArrayList<Tick>();
		
		final int numItems = pvList.size();				
			
		//process data from last - first so that we can calculate the moving average
		for(int i = numItems-1; i>=0; i--) {
			final PriceVolumeData currOne = pvList.get(i);
			
			final Tick aTick = new Tick();
			aTick.setDate(currOne.getTradeDateAsDate());
			aTick.setTradeDateType(currOne.getTradeDateType());
			
			if(aTick.getTradeDateType() == TradeDateType.TRADE_DATE) {
				final Price price = new Price();
				price.setClose(currOne.getClose());
				price.setOpen(currOne.getOpen());
				price.setHigh(currOne.getHigh());
				price.setLow(currOne.getLow());
				
				aTick.setPrice(price);
				
				final Volume volume = new Volume();
				volume.setVolume(currOne.getVolume());
				aTick.setVolume(volume);
			}
			ticks.add(aTick);
			
			priceTickLabelHelper.processTick(pvList, aTick, i);
			
			if(priceMA1 != null) {
				final MovingAverage  avg = priceMA1.processTick(ticks, numItems-i-1);
				aTick.setPriceMovingAverage1(avg);
			}
			if(priceMA2 != null) {
				final MovingAverage  avg = priceMA2.processTick(ticks, numItems-i-1);
				aTick.setPriceMovingAverage2(avg);
			}
		}
		
		// add extra ticks to the chart
		for(int i = 0; i<NUM_TICKS_EXTRA;i++) {
			final Tick lastTick = ticks.get(ticks.size() - 1);
			
			Date dt = null;
			switch(periodicity) {
			case Daily : {
				dt = CalendarUtil.addOneWeekDay(lastTick.getDate());
				break;
			}
			case Weekly: {
				dt = CalendarUtil.addWeeks(lastTick.getDate(), 1);
				break;			
			}
			case Monthly: {
				dt = CalendarUtil.addMonths(lastTick.getDate(), 1);
				break;			
			}	
			}
			
			final Tick aTick = new Tick();
			aTick.setDate(dt);
			aTick.setTradeDateType(TradeDateType.NO_TRADE);
			
			ticks.add(aTick);
		}
		
		//reverse the ticks, so the latest is on the beginning
		Collections.reverse(ticks);
				
		elements.setTicks(ticks);
		elements.setFinancials(chartData.getFinancialData());
		
		populateChartElements(elements, benchmarkChartData, chartBounds, isLinear, tickSpacing);
		return elements;
	}

	public static void populateChartElements(final ChartElementsData elements, final ChartData benchmarkChartData, final ChartBoundsRect chartBounds, final boolean isLinear, final int tickSpacing) {
		// if we dont have any chart-specific data, lets return this method
		if(chartBounds == null || elements == null || elements.getTicks() == null) {
			return;
		}

		final PeriodicityType periodicity = elements.getPeriodicity();
		IChartHelper chartHelper = null;
		  
		if(periodicity == PeriodicityType.Daily) {
			chartHelper = new DailyChartHelper(chartBounds, elements.getTicks(), isLinear, tickSpacing);
		} else if (periodicity == PeriodicityType.Weekly) {
			chartHelper = new WeeklyChartHelper(chartBounds, elements.getTicks(), isLinear, tickSpacing);
		} else if (periodicity == PeriodicityType.Monthly) {
			chartHelper = new MonthlyChartHelper(chartBounds, elements.getTicks(), isLinear, tickSpacing);
		}

		final List<Tick> ticks = elements.getTicks();
		for(final Tick aTick : ticks) {
			chartHelper.processTick(aTick);
		}
		
		final DateAxis dateAxis = chartHelper.getDateAxis();
		elements.setDateAxis(dateAxis);
		elements.setPriceAxis(chartHelper.getPriceAxis());
		elements.setVerticalGridLines(chartHelper.getVerticalGridLines());
		elements.setHorizontalGridLines(chartHelper.getHorizontalGridLines());
		
		//process benchmark ticks
		if(benchmarkChartData != null && benchmarkChartData.getPrices() != null) {
			double benchmarkLow = Double.MAX_VALUE;
			double benchmarkHigh = Double.MIN_VALUE;
					
			//final List<BenchmarkLine> bLines = new ArrayList<BenchmarkLine>();
			final int numBTicks = benchmarkChartData.getPrices().size();
			BigDecimal lastBenchmarkClose = null;
			
			for(int i=1;i<=numBTicks;i++) {
				final int index = numBTicks - i;
				final PriceVolumeData aData = benchmarkChartData.getPrices().get(index);
				if(aData.getAdjClose() != null) {
					if(lastBenchmarkClose == null) {
						lastBenchmarkClose = aData.getAdjClose();
					}
					
					final BenchmarkLine bl = new BenchmarkLine();
					final Date dt = aData.getTradeDateAsDate(); 
					bl.setDate(dt);
					double benchmarkClose = aData.getAdjClose().doubleValue();
					bl.setPrice(benchmarkClose);
					
					benchmarkLow = benchmarkLow > benchmarkClose ? benchmarkClose : benchmarkLow;
					benchmarkHigh = benchmarkHigh < benchmarkClose ? benchmarkClose : benchmarkHigh;

					final int tickIndex = dateAxis.getTickIndex(dt);
					if(tickIndex < 0 || tickIndex >= ticks.size()) {
						continue;
					}
					final Tick aTick = ticks.get(tickIndex);
					if(aTick.getDate().equals(dt)) {
						aTick.setBenchmarkLine(bl);
						
						if(aTick.getTradeDateType() == TradeDateType.TRADE_DATE) {
							//calculate relative-strength line
							final RelativeStrengthLine rsLine = new RelativeStrengthLine();
							rsLine.setDate(dt);
						
							final double rsVal = RS_FACTOR * (aTick.getPrice().getClose().doubleValue() * lastBenchmarkClose.doubleValue()) / benchmarkClose;
						
							rsLine.setPrice(rsVal);
							aTick.setRsLine(rsLine);
						}
					}
					
					//bLines.add(bl);
				}
			}
			// set the benchmark axis
			final BenchmarkAxis benchmarkAxis = new BenchmarkAxis(benchmarkLow, benchmarkHigh);
			elements.setBenchmarkAxis(benchmarkAxis);
			
			//elements.setBenchmarkLine(bLines);
		}
		
	}

	
	public static final void dashLineTo(final Context2d context,
			final double x, final double y,final double x2,final double y2,
			final int dashLength, final int gapLength){
		DashedLineRenderer.drawDashedLine(context, x, y, x2, y2, dashLength, gapLength);
	}
	
	public static void lineTo(final Context2d context,
			final double x, final double y,final double x2,final double y2){
	
		context.moveTo(x, y);
		context.lineTo(x2, y2);
  }

	public static void roundedRect(Context2d ctx,int x,int y, int width,int height,int radius) {
		  ctx.beginPath();
		  ctx.moveTo(x,y+radius);
		  ctx.lineTo(x,y+height-radius);
		  ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
		  ctx.lineTo(x+width-radius,y+height);
		  ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
		  ctx.lineTo(x+width,y+radius);
		  ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
		  ctx.lineTo(x+radius,y);
		  ctx.quadraticCurveTo(x,y,x,y+radius);
		  ctx.stroke();
	}
}

class PriceMovingAverageHelper {
	private int length = 0;
	private boolean firstMA = false;
	public PriceMovingAverageHelper(int length, boolean firstMA) {
		this.length = length;
		this.firstMA = firstMA;
	}
	
	private double rollingSum = 0;
	private List<Tick> rollingTicks = new ArrayList<Tick>();
	public MovingAverage processTick(final List<Tick> allTicks, final int index) {
		final Tick aTick = allTicks.get(index);
		
		MovingAverage avg = null;
		if (aTick.getTradeDateType() == TradeDateType.TRADE_DATE) {
			// its a trade-date
			rollingTicks.add(aTick);
			rollingSum += aTick.getPrice().getClose().doubleValue();

			if(rollingTicks.size() > length) {
				final Tick lastTick = rollingTicks.remove(0);
				rollingSum -= lastTick.getPrice().getClose().doubleValue();
				
				avg = new MovingAverage();
				avg.setValue(rollingSum / length);
			} else {
				
			}
		} else {
			// for non-trading-dates we return previous tick's MA
			if(index > 0) {
				final Tick previousTick = allTicks.get(index-1);
				avg = getMovingAverageFromTick(previousTick);
			}
		}
		
		return avg;
	}
	
	private MovingAverage getMovingAverageFromTick(final Tick aTick) {
		if (firstMA) {
			return aTick.getPriceMovingAverage1();
		} else {
			return aTick.getPriceMovingAverage2();
		}
	}
}

class PriceTickLabelHelper {
	private static final int LOOKAHEAD = 7;
	
	public PriceTickLabelHelper(){
	}
	
	public void processTick(final List<PriceVolumeData> pvList, final Tick aTick, final int index) {
		final PriceVolumeData aData = pvList.get(index);
		if (aData.getTradeDateType() == TradeDateType.TRADE_DATE) {
			boolean isHigh = true;
			boolean isLow = true;
			for(int i=1; i<LOOKAHEAD;i++) {
				//lookahead first
				int lookNextIndex = index + i;
				if(lookNextIndex >= pvList.size() || lookNextIndex < 0) {
					isHigh = false;
					isLow = false;
					break;
				}
				
				PriceVolumeData nextTick = pvList.get(lookNextIndex);
				if(nextTick.getTradeDateType() == TradeDateType.TRADE_DATE &&
						nextTick.getHigh().compareTo(aTick.getPrice().getHigh()) >= 0) {
					isHigh = false;
				}
				if(nextTick.getTradeDateType() == TradeDateType.TRADE_DATE &&
						nextTick.getLow().compareTo(aTick.getPrice().getLow()) <= 0) {
					isLow = false;
				}

				if(!isHigh && !isLow) {
					break;
				}
				
				lookNextIndex = index - i;
				if(lookNextIndex < 0) {
					isHigh = false;
					isLow = false;
					break;
				}
				
				// lookback now
				nextTick = pvList.get(lookNextIndex);
				if(nextTick.getTradeDateType() == TradeDateType.TRADE_DATE &&
						nextTick.getLow().compareTo(aTick.getPrice().getLow()) < 0) {
					isLow = false;
				}
				if(nextTick.getTradeDateType() == TradeDateType.TRADE_DATE &&
						nextTick.getHigh().compareTo(aTick.getPrice().getHigh()) > 0) {
					isHigh = false;
				}

				if(!isHigh && !isLow) {
					break;
				}
			}
			if(isLow) {
				aTick.setHasPriceLowLabel(isLow);
			}
			if(isHigh) {
				aTick.setHasPriceHighLabel(isHigh);
			}
		}
	}
}